Skip to content

fix: sync MCP sessions to Redis for multi-replica deployments - #3491

Merged
JanCizmar merged 1 commit into
mainfrom
dbocharov/fix/clustered-mcp-server
Feb 26, 2026
Merged

fix: sync MCP sessions to Redis for multi-replica deployments#3491
JanCizmar merged 1 commit into
mainfrom
dbocharov/fix/clustered-mcp-server

Conversation

@bdshadow

@bdshadow bdshadow commented Feb 25, 2026

Copy link
Copy Markdown
Member

WebMvcStreamableServerTransportProvider stores sessions in an in-memory ConcurrentHashMap, causing 404 "Session not found" errors when requests scatter across replicas. Add a servlet filter that persists session metadata to Redis and reconstructs sessions on replicas that haven't seen the client.

Tracked upstream: modelcontextprotocol/java-sdk#201

Summary by CodeRabbit

  • New Features

    • MCP sessions are now optionally persisted to Redis and synchronized across instances, enabling automatic session recovery and continuity for up to 48 hours (no-op when Redis is not configured).
  • Tests

    • Added integration tests validating end-to-end Redis-backed session persistence and recovery across replicas.

@coderabbitai

coderabbitai Bot commented Feb 25, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds Redis-backed MCP session persistence and recovery: a new servlet filter, DTO, and Spring bean; Redisson dependency promoted to runtime; and an integration test that verifies session recovery after local eviction.

Changes

Cohort / File(s) Summary
Dependency Configuration
backend/app/build.gradle
Promoted libs.redissonSpringBootStarter from testApi to implementation, making Redisson available at runtime.
MCP session runtime
backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionData.kt, .../McpSessionRedisFilter.kt, .../McpConfig.kt
Added McpSessionData DTO; implemented McpSessionRedisFilter (restores sessions from Redis when missing locally, persists new sessions to Redis with 48h TTL and mcp_session: prefix, uses reflection to access provider internals, no-op if Redisson absent); registered filter bean in McpConfig.
Integration tests
backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt
Added integration test that starts Redis, creates an MCP session, evicts the local session entry, then verifies recovery from Redis via a follow-up request and reflection-based inspection.

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Filter as McpSessionRedisFilter
    participant Local as Local Sessions Map
    participant Redis as Redis
    participant Factory as Session Factory

    rect rgba(100,150,200,0.5)
    Note over Client,Filter: Incoming request to /mcp/* with MCP-Session-Id
    Client->>Filter: HTTP request (MCP-Session-Id)
    Filter->>Local: check session map for id
    Local-->>Filter: session missing
    Filter->>Redis: GET mcp_session:<id>
    Redis-->>Filter: session data (JSON)
    Filter->>Local: restore session into sessions map
    Filter->>Factory: forward request to session factory
    end

    rect rgba(150,200,100,0.5)
    Note over Factory,Filter: Request handling may create new session
    Factory-->>Filter: response with MCP-Session-Id header
    Filter->>Redis: SET mcp_session:<id> (data) EX 172800
    Redis-->>Filter: ACK
    Filter-->>Client: HTTP response
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • Anty0

Poem

🐇 I hopped through headers, keys in paw,
I fetched lost sessions from Redis' maw,
When local maps let IDs slip away,
I stitch them back before break of day,
Now MCPs persist while I munch hay.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: sync MCP sessions to Redis for multi-replica deployments' accurately and concisely summarizes the main change: adding Redis-backed session synchronization to address multi-replica deployment issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dbocharov/fix/clustered-mcp-server

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bdshadow
bdshadow force-pushed the dbocharov/fix/clustered-mcp-server branch 4 times, most recently from 3f51675 to cd00393 Compare February 25, 2026 13:30
@bdshadow
bdshadow requested a review from JanCizmar February 25, 2026 13:52
@bdshadow
bdshadow marked this pull request as ready for review February 25, 2026 13:52

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt (1)

65-94: Good integration test covering the core recovery flow.

The test cleanly validates the end-to-end scenario: session creation → Redis persistence → local eviction → recovery → successful tool call. The use of reflection to access the sessions map mirrors the filter's approach, keeping the test realistic.

One minor gap: the test doesn't assert that the recovered session's capabilities/client info match the original. If the serialization/deserialization round-trip has a bug, the session would exist in the map but with null/wrong metadata.

Optional: verify session data fidelity
// After step 5, verify the recovered session has correct data
val recoveredSession = sessionsMap[sessionId]!!
// Use the same reflection approach as the filter to check clientInfo is non-null
val clientInfoField = McpStreamableServerSession::class.java.getDeclaredField("clientInfo")
clientInfoField.isAccessible = true
val clientInfo = (clientInfoField.get(recoveredSession) as? java.util.concurrent.atomic.AtomicReference<*>)?.get()
assertThat(clientInfo).isNotNull
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`
around lines 65 - 94, Update the test method session is recovered from Redis
after local eviction to also verify the recovered session's metadata: after step
5 retrieve the recoveredSession from sessionsMap using sessionId, use reflection
on McpStreamableServerSession to access the private clientInfo field (make it
accessible), unwrap the AtomicReference to get the underlying clientInfo object
and assert it is not null so the serialization/deserialization preserved client
info; reference symbols: McpRedisSessionRecoveryTest, sessionsMap, sessionId,
McpStreamableServerSession, clientInfo.
backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt (2)

87-118: Recovered session lacks SSE transport — subsequent streaming may fail.

The McpStreamableServerSession constructed here has clientCapabilities, clientInfo, requestTimeout, and handlers — but it does not restore the session's SSE transport or any active subscriptions. If the MCP protocol requires server-to-client push (e.g., notifications, progress updates) on a recovered session, those will silently fail because no transport is connected.

This is likely acceptable for request/response tool calls (which is what the test covers), but worth documenting as a known limitation so that future developers don't assume full session fidelity after recovery.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt` around
lines 87 - 118, The recovered session created in recoverSessionFromRedis using
McpStreamableServerSession lacks any SSE transport or subscription state, so
server-to-client streaming/notifications will silently fail; update
recoverSessionFromRedis to restore the SSE transport and any active
subscriptions from McpSessionData if those fields exist (rehydrate transport and
subscription lists into the new McpStreamableServerSession and reattach into
sessionsMap), and if no transport/subscriptions are stored, explicitly mark the
session as non-streamable and log a clear warning so callers know streaming is
unavailable; reference McpStreamableServerSession, McpSessionData,
recoverSessionFromRedis and sessionsMap when making the changes.

226-230: Consider making REDIS_KEY_PREFIX configurable or include a version discriminator.

If the SDK constructor signature changes across versions, stale Redis entries written by an older version could be deserialized into an incompatible session on a newer replica. Including a schema version in the key prefix (e.g., mcp_session:v1:) would let you invalidate old entries on upgrade without manual Redis flushes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt` around
lines 226 - 230, REDIS_KEY_PREFIX is currently hardcoded in the
McpSessionRedisFilter companion object; make the Redis key prefix configurable
or include a schema/version discriminator (e.g., "mcp_session:v1:") so old
entries can be invalidated on upgrades. Change REDIS_KEY_PREFIX to be
injected/read from configuration (or build it from an application property like
mcp.session.redisPrefix with default "mcp_session:v1:") and update all places
that reference REDIS_KEY_PREFIX in McpSessionRedisFilter to use the new
configurable value; ensure the default includes the version token and document
the property for future version bumps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 69-84: The pre-handle check in McpSessionRedisFilter uses
sessionsMap.containsKey(sessionId) then calls
recoverSessionFromRedis(sessionId), which can cause concurrent threads to
rebuild the same McpStreamableServerSession and overwrite transport/SSE state;
modify recoverSessionFromRedis (or the call site) to avoid duplicate
construction by using a concurrent insertion: attempt to insert the recovered
session into sessionsMap with computeIfAbsent or use putIfAbsent after
constructing the McpStreamableServerSession and discard/close the newly created
one if another thread won the race, ensuring only the winner's transport/SSE is
kept; keep the MCP_SESSION_ID_HEADER lookup and response capture logic
unchanged.
- Around line 125-130: The cast of clientCapabilities/clientInfo to
java.util.concurrent.atomic.AtomicReference in McpSessionRedisFilter (via
getPrivateField) is fragile and can silently yield null; change the logic to
detect when the retrieved field is not an AtomicReference: if it is an
AtomicReference, use .get() as before; otherwise attempt to use the field value
directly as the capabilities/info and emit a warning via the class logger
(include field name and actual field type) so you surface SDK changes; keep the
existing behavior when values end up null but ensure the warning is logged to
aid debugging (refer to clientCapabilities, clientInfo, capabilitiesValue,
infoValue, getPrivateField).
- Around line 103-112: Add a startup-time smoke check that attempts to
instantiate McpStreamableServerSession to fail fast if the SDK constructor
signature changes: in the McpSessionRedisFilter (or a dedicated `@Component`) add
a `@PostConstruct` or ApplicationReadyEvent handler which constructs a
minimal/dummy McpStreamableServerSession using the same positional args used in
your deserialization path (sessionId, clientCapabilities, clientInfo,
factoryFields.requestTimeout, factoryFields.requestHandlers,
factoryFields.notificationHandlers) and log or rethrow any exception; this uses
the same symbols (McpStreamableServerSession, McpSessionRedisFilter,
factoryFields) as the recovery flow and complements McpRedisSessionRecoveryTest
by validating constructor compatibility at startup.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`:
- Around line 60-63: Add an `@AfterEach` cleanup that deletes the test data
created in setup(): call the inverse cleanup for createTestDataWithPat() (e.g.,
deleteTestData or removeTestData) inside a method annotated with `@AfterEach` in
McpRedisSessionRecoveryTest so each test method's data is removed; ensure the
cleanup method references the same test data holder (the variable data)
initialized in setup() and runs after each test to avoid accumulation between
tests.

---

Nitpick comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 87-118: The recovered session created in recoverSessionFromRedis
using McpStreamableServerSession lacks any SSE transport or subscription state,
so server-to-client streaming/notifications will silently fail; update
recoverSessionFromRedis to restore the SSE transport and any active
subscriptions from McpSessionData if those fields exist (rehydrate transport and
subscription lists into the new McpStreamableServerSession and reattach into
sessionsMap), and if no transport/subscriptions are stored, explicitly mark the
session as non-streamable and log a clear warning so callers know streaming is
unavailable; reference McpStreamableServerSession, McpSessionData,
recoverSessionFromRedis and sessionsMap when making the changes.
- Around line 226-230: REDIS_KEY_PREFIX is currently hardcoded in the
McpSessionRedisFilter companion object; make the Redis key prefix configurable
or include a schema/version discriminator (e.g., "mcp_session:v1:") so old
entries can be invalidated on upgrades. Change REDIS_KEY_PREFIX to be
injected/read from configuration (or build it from an application property like
mcp.session.redisPrefix with default "mcp_session:v1:") and update all places
that reference REDIS_KEY_PREFIX in McpSessionRedisFilter to use the new
configurable value; ensure the default includes the version token and document
the property for future version bumps.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`:
- Around line 65-94: Update the test method session is recovered from Redis
after local eviction to also verify the recovered session's metadata: after step
5 retrieve the recoveredSession from sessionsMap using sessionId, use reflection
on McpStreamableServerSession to access the private clientInfo field (make it
accessible), unwrap the AtomicReference to get the underlying clientInfo object
and assert it is not null so the serialization/deserialization preserved client
info; reference symbols: McpRedisSessionRecoveryTest, sessionsMap, sessionId,
McpStreamableServerSession, clientInfo.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 007f2f6 and cd00393.

📒 Files selected for processing (5)
  • backend/app/build.gradle
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpConfig.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionData.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
  • backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt

Comment thread backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
Comment thread backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
Comment thread backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt Outdated
@bdshadow
bdshadow force-pushed the dbocharov/fix/clustered-mcp-server branch 2 times, most recently from 7282bfa to b9666ad Compare February 25, 2026 14:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt (1)

72-74: ⚠️ Potential issue | 🟡 Minor

Avoid non-atomic recover/insert for the same session ID.

Line 72 + Line 114 allow concurrent requests to reconstruct and overwrite the same session. Use atomic insertion (putIfAbsent/computeIfAbsent) to prevent duplicate construction and overwrite races.

💡 Suggested concurrency-safe adjustment
-      sessionsMap[sessionId] = session
+      sessionsMap.putIfAbsent(sessionId, session)
-    if (sessionId != null && !sessionsMap.containsKey(sessionId)) {
+    if (sessionId != null && !sessionsMap.containsKey(sessionId)) {
       recoverSessionFromRedis(sessionId)
     }

(Keep the pre-check, but ensure final insertion is atomic.)

Also applies to: 114-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt` around
lines 72 - 74, The current non-atomic pre-check + recover flow can lead to
duplicate/overwriting sessions for the same sessionId; update the insertion into
sessionsMap to be atomic (use sessionsMap.putIfAbsent(sessionId, session) or
sessionsMap.computeIfAbsent(sessionId, id -> recoveredSession)) in the
recoverSessionFromRedis flow so concurrent threads cannot reconstruct and
overwrite the same session; keep the existing pre-check (if (sessionId != null
&& !sessionsMap.containsKey(sessionId)) ...) but ensure the final store uses an
atomic method in McpSessionRedisFilter where sessionId, sessionsMap and
recoverSessionFromRedis are used.
backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt (1)

60-63: ⚠️ Potential issue | 🟡 Minor

Add per-test cleanup for created TestData.

This test creates persisted data in @BeforeEach but does not clean it in @AfterEach. Please add cleanup for data.testData to keep isolation between test methods and avoid data accumulation.

As per coding guidelines "Use TestData classes for test setup following the pattern: create TestData instance, save with testDataService, and clean in @AfterEach".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`
around lines 60 - 63, The setup() method calls createTestDataWithPat() and
stores the result in data but there's no per-test cleanup; add an `@AfterEach`
method that calls testDataService.delete(data.testData) (or the TestData cleanup
helper used elsewhere) to remove the persisted TestData after each test,
ensuring you reference the same data variable produced by
createTestDataWithPat() and import/annotate the cleanup method with `@AfterEach`
so test isolation is preserved.
🧹 Nitpick comments (1)
backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt (1)

218-220: Use case-insensitive header name matching in wrapper.

Line 218 and Line 228 compare header names case-sensitively. Use equals(..., ignoreCase = true) so alternate casing still captures Mcp-Session-Id.

Also applies to: 228-230

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt` around
lines 218 - 220, In the header-wrapping logic inside McpSessionRedisFilter (when
comparing incoming header names to MCP_SESSION_ID_HEADER) change the
case-sensitive comparisons that use == to use equals(..., ignoreCase = true) so
alternate header casing (e.g., "Mcp-Session-Id") is captured; update both places
where name == MCP_SESSION_ID_HEADER (the block that sets capturedSessionId and
the later comparison around lines referencing capturedSessionId) to use
MCP_SESSION_ID_HEADER.equals(name, ignoreCase = true).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 71-74: Wrap the reflective local-map lookup around
sessionsMap.containsKey(sessionId) in a safe try/catch inside
McpSessionRedisFilter pre-handle so reflective failures don't throw: call
sessionsMap.containsKey(sessionId) inside a small try block and treat any caught
Exception as "unknown / false" (i.e., skip recovery) so that
recoverSessionFromRedis(sessionId) is only invoked when the containsKey check
safely returns false; reference the sessionsMap field and
recoverSessionFromRedis method in the change.

---

Duplicate comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 72-74: The current non-atomic pre-check + recover flow can lead to
duplicate/overwriting sessions for the same sessionId; update the insertion into
sessionsMap to be atomic (use sessionsMap.putIfAbsent(sessionId, session) or
sessionsMap.computeIfAbsent(sessionId, id -> recoveredSession)) in the
recoverSessionFromRedis flow so concurrent threads cannot reconstruct and
overwrite the same session; keep the existing pre-check (if (sessionId != null
&& !sessionsMap.containsKey(sessionId)) ...) but ensure the final store uses an
atomic method in McpSessionRedisFilter where sessionId, sessionsMap and
recoverSessionFromRedis are used.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`:
- Around line 60-63: The setup() method calls createTestDataWithPat() and stores
the result in data but there's no per-test cleanup; add an `@AfterEach` method
that calls testDataService.delete(data.testData) (or the TestData cleanup helper
used elsewhere) to remove the persisted TestData after each test, ensuring you
reference the same data variable produced by createTestDataWithPat() and
import/annotate the cleanup method with `@AfterEach` so test isolation is
preserved.

---

Nitpick comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 218-220: In the header-wrapping logic inside McpSessionRedisFilter
(when comparing incoming header names to MCP_SESSION_ID_HEADER) change the
case-sensitive comparisons that use == to use equals(..., ignoreCase = true) so
alternate header casing (e.g., "Mcp-Session-Id") is captured; update both places
where name == MCP_SESSION_ID_HEADER (the block that sets capturedSessionId and
the later comparison around lines referencing capturedSessionId) to use
MCP_SESSION_ID_HEADER.equals(name, ignoreCase = true).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd00393 and 7282bfa.

📒 Files selected for processing (5)
  • backend/app/build.gradle
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpConfig.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionData.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
  • backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt

Comment thread backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
WebMvcStreamableServerTransportProvider stores sessions in an in-memory
ConcurrentHashMap, causing 404 "Session not found" errors when requests
scatter across replicas. Add a servlet filter that persists session
metadata to Redis and reconstructs sessions on replicas that haven't
seen the client.

Tracked upstream: modelcontextprotocol/java-sdk#201

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@bdshadow
bdshadow force-pushed the dbocharov/fix/clustered-mcp-server branch from b9666ad to d436271 Compare February 25, 2026 14:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt (1)

71-74: ⚠️ Potential issue | 🟠 Major

Guard pre-handle local map lookup from reflective failures.

Line 72 can throw if reflective map loading breaks (SDK internals change), and this path is currently not protected, so request handling can fail before reaching fallback behavior.

💡 Suggested hardening
-    if (sessionId != null && !sessionsMap.containsKey(sessionId)) {
-      recoverSessionFromRedis(sessionId)
-    }
+    if (sessionId != null) {
+      val missingLocally =
+        runCatching { !sessionsMap.containsKey(sessionId) }
+          .onFailure { log.warn("Failed to access MCP local sessions map; skipping Redis recovery", it) }
+          .getOrDefault(false)
+      if (missingLocally) {
+        recoverSessionFromRedis(sessionId)
+      }
+    }
#!/bin/bash
set -euo pipefail

FILE="$(fd "McpSessionRedisFilter.kt" --type f | head -1)"
echo "Inspecting: $FILE"
echo
echo "=== Pre-handle block ==="
sed -n '68,78p' "$FILE"
echo
echo "=== sessionsMap access context ==="
rg -n 'sessionsMap\.containsKey\(|recoverSessionFromRedis\(' "$FILE" -C2

Expected result: the pre-handle containsKey call is shown directly in the condition, without local error-guarding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt` around
lines 71 - 74, The containsKey check on sessionsMap in the pre-handle block can
throw if reflective map loading breaks; wrap the
sessionsMap.containsKey(sessionId) call in a protective try-catch (or safe
helper) inside the pre-handle so any Throwable from the reflective lookup is
caught and treated as “not present”, then call
recoverSessionFromRedis(sessionId) as the fallback; update the logic around the
existing sessionsMap / recoverSessionFromRedis usage so exceptions from
containsKey do not propagate out of the pre-handle.
🧹 Nitpick comments (1)
backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt (1)

84-84: Make session selection deterministic in the test.

Using first() on ConcurrentHashMap keys can become order-dependent if extra sessions exist, which can make this test flaky.

✅ Suggested tweak
-    val sessionId = sessionsMap.keys().toList().first()
+    val sessionId =
+      sessionsMap.keys().singleOrNull()
+        ?: error("Expected exactly one MCP session in map, got ${sessionsMap.keys()}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt` at
line 84, The test McpRedisSessionRecoveryTest uses
sessionsMap.keys().toList().first() which is non-deterministic; make selection
deterministic by ordering the keys before picking one (e.g., use
sorted()/minOrNull() on sessionsMap.keys()) so sessionId is consistently the
same across runs and the test stops being flaky.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt`:
- Around line 71-74: The containsKey check on sessionsMap in the pre-handle
block can throw if reflective map loading breaks; wrap the
sessionsMap.containsKey(sessionId) call in a protective try-catch (or safe
helper) inside the pre-handle so any Throwable from the reflective lookup is
caught and treated as “not present”, then call
recoverSessionFromRedis(sessionId) as the fallback; update the logic around the
existing sessionsMap / recoverSessionFromRedis usage so exceptions from
containsKey do not propagate out of the pre-handle.

---

Nitpick comments:
In `@backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt`:
- Line 84: The test McpRedisSessionRecoveryTest uses
sessionsMap.keys().toList().first() which is non-deterministic; make selection
deterministic by ordering the keys before picking one (e.g., use
sorted()/minOrNull() on sessionsMap.keys()) so sessionId is consistently the
same across runs and the test stops being flaky.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7282bfa and d436271.

📒 Files selected for processing (5)
  • backend/app/build.gradle
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpConfig.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionData.kt
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpSessionRedisFilter.kt
  • backend/app/src/test/kotlin/io/tolgee/mcp/McpRedisSessionRecoveryTest.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • backend/app/src/main/kotlin/io/tolgee/mcp/McpConfig.kt

@JanCizmar
JanCizmar merged commit 3f25d44 into main Feb 26, 2026
222 of 234 checks passed
@JanCizmar
JanCizmar deleted the dbocharov/fix/clustered-mcp-server branch February 26, 2026 07:56
TolgeeMachine added a commit that referenced this pull request Feb 26, 2026
## [3.163.3](v3.163.2...v3.163.3) (2026-02-26)

### Bug Fixes

* sync MCP sessions to Redis for multi-replica deployments ([#3491](#3491)) ([3f25d44](3f25d44))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants